// ========================================================================
// Copyright (c) 2010 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================
/**
 * Copyright 2011 Ivan Hristov
 *
 * hristov.iv [AT] gmail [DOT] com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 */
package com.google.code.johanna.scala.websocket.client
import org.eclipse.jetty.io.EndPoint
import org.eclipse.jetty.io.Buffer
import java.util.Random
import java.security.SecureRandom
import org.eclipse.jetty.io.EofException
import java.io.IOException

/**
 *
 * @author Ivan Hristov
 *
 */
trait MaskGen {
  def genMask(mask: Array[Byte]);
}

class NullMaskGen extends MaskGen {
  def genMask(mask: Array[Byte]) {
    mask(0) = 0
    mask(1) = 0
    mask(2) = 0
    mask(3) = 0
  }
}

class FixedMaskGen extends MaskGen {
  var _mask: Array[Byte] = new Array[Byte](4);
  _mask(0)= 0xff.toByte
  _mask(1)= 0xff.toByte
  _mask(2)= 0xff.toByte
  _mask(3)= 0xff.toByte
  
  def this(mask: Array[Byte]) {
    this()
    _mask = mask;
  }

  def genMask(mask: Array[Byte]) {
    mask(0) = _mask(0);
    mask(1) = _mask(1);
    mask(2) = _mask(2);
    mask(3) = _mask(3);
  }
}

class RandomMaskGen extends MaskGen {
  var _random: Random = new SecureRandom();
  

  def this(random: Random) {
    this()
    _random = random;
  }

  def genMask(mask: Array[Byte]) {
    _random.nextBytes(mask);
  }
}

class WebSocketFrameProperties(var opcode: Byte, var offset: Int, var length: Int) {
}

class WebSocketGeneratorD7_9(var buffers: WebSocketBuffers, var endp: EndPoint) extends WebSocketGenerator {
  val OP_CONTINUATION: Byte = 0x00;
	var _buffer: Buffer = null;

  val _mask: Array[Byte] = new Array[Byte](4)
  
  
  var _maskGen: MaskGen = null;

  def this(buffers: WebSocketBuffers, endp: EndPoint, maskGen: MaskGen) {
    this(buffers, endp)
    _maskGen = maskGen;
  }

  def isLastFrame(flags: Byte): Boolean = {
    { flags & 0x8 } != 0;
  }

  def addFrame(flags: Byte, content: Array[Byte], frameProperties : WebSocketFrameProperties) {
    // System.err.printf("<< %s %s %s\n",TypeUtil.toHexString(flags),TypeUtil.toHexString(opcode),length);

    val mask: Boolean = _maskGen != null;

    if (_buffer == null)
      _buffer = if (mask) buffers.getBuffer() else buffers.getDirectBuffer();

    val last: Boolean = isLastFrame(flags);
    val orig: Byte = frameProperties.opcode

    val space: Int = if (mask) 14 else 10;

    var _opsent: Boolean = true;
    do {
      frameProperties.opcode = if (_opsent) OP_CONTINUATION else  frameProperties.opcode;
       frameProperties.opcode = (((0xf & flags) << 4) + (0xf &  frameProperties.opcode)).toByte;
      


      var payload: Int = frameProperties.length;
      if (payload + space > _buffer.capacity()) {
        // We must fragement, so clear FIN bit
        frameProperties.opcode = (frameProperties.opcode & 0x7F).toByte; // Clear the FIN bit
        payload = _buffer.capacity() - space;
      } else if (last)
        frameProperties.opcode = (frameProperties.opcode | 0x80).toByte; // Set the FIN bit

      // ensure there is space for header
      if (_buffer.space() <= space) {
        flushBuffer();
        if (_buffer.space() <= space)
          flush();
      }

      // write the opcode and length
      if (payload > 0xffff) {
        if (mask) {
          _buffer.put(Array[Byte](frameProperties.opcode, 0xff.toByte));
        } else {
          val tmpArray: Array[Byte] = Array[Byte](frameProperties.opcode, 0x7f.toByte);
          tmpArray(3) = ((payload >> 56) & 0x7f).toByte
          tmpArray(4) = ((payload >> 48) & 0xff).toByte
          tmpArray(5) = ((payload >> 40) & 0xff).toByte
          tmpArray(6) = ((payload >> 32) & 0xff).toByte
          tmpArray(7) = ((payload >> 24) & 0xff).toByte
          tmpArray(8) = ((payload >> 16) & 0xff).toByte
          tmpArray(9) = ((payload >> 8) & 0xff).toByte
          tmpArray(10) = (payload & 0xff).toByte
          _buffer.put(tmpArray);

        }
      } else if (payload >= 0x7e) {
        _buffer.put(Array[Byte](
          frameProperties.opcode,
          if (mask) 0xfe.toByte else
            0x7e.toByte,
          (payload >> 8).toByte,
          (payload & 0xff).toByte));
      } else {
        _buffer.put(Array[Byte](
          frameProperties.opcode,
          if (mask) (0x80 | payload).toByte else payload.toByte));
      }
      
      var _m: Int = 0;
      // write mask
      if (mask) {
        _maskGen.genMask(_mask);
        _m = 0;
        _buffer.put(_mask);
      }

      // write payload
      var remaining: Int = payload;
      while (remaining > 0) {
        _buffer.compact();
        val chunk: Int = if (remaining < _buffer.space()) remaining else _buffer.space();

        if (mask) {
          for (i <- 0 until chunk) {

            _m += 1
            +_m
            val tmp: Int = _m.%(4)
            _buffer.put((content(frameProperties.offset + (payload - remaining) + i) ^ _mask(tmp)).toByte);
          }
        } else
          _buffer.put(content, frameProperties.offset + (payload - remaining), chunk);

        remaining -= chunk;
        if (_buffer.space() > 0) {
          // Gently flush the data, issuing a non-blocking write
          flushBuffer();
        } else {
          // Forcibly flush the data, issuing a blocking write
          flush();
          if (remaining == 0) {
            // Gently flush the data, issuing a non-blocking write
            flushBuffer();
          }
        }
      }
      frameProperties.offset += payload;
      frameProperties.length -= payload;
    } while (frameProperties.length > 0);
    _opsent = last.unary_!;

    if (_buffer != null && _buffer.length() == 0) {
      buffers.returnBuffer(_buffer);
      _buffer = null;
    }
  }

  def flushBuffer(): Int = {
    if (!endp.isOpen())
      throw new EofException();

    if (_buffer != null)
      return endp.flush(_buffer);

    return 0;
  }

  def flush(): Int = {
    if (_buffer == null)
      return 0;
    var result: Int = flushBuffer();
    if (!endp.isBlocking()) {
      var now: Long = System.currentTimeMillis();
      val end: Long = now + endp.getMaxIdleTime();
      while (_buffer.length() > 0) {
        val ready: Boolean = endp.blockWritable(end - now);
        if (!ready) {
          now = System.currentTimeMillis();
          if (!(now < end)) {
            throw new IOException("Write timeout");
          }
        }
        if (!(now < end)) {
          result += flushBuffer();
        }
      }
    }
    _buffer.compact();
    return result;
  }

  def isBufferEmpty () : Boolean = {
    _buffer == null || _buffer.length() == 0;
  }

  def idle () {
    if (_buffer != null && _buffer.length() == 0) {
      buffers.returnBuffer(_buffer);
      _buffer = null;
    }
  }

}